package edos.prm.process;

import java.lang.reflect.Constructor;
import java.util.Set;

import edos.prm.activity.Operation;
import edos.prm.actor.Actor;
import edos.prm.process.Environment.Variable;
import edos.prm.role.Right;
import edos.prm.role.Role;
import edos.prm.role.RoleException;

/**
 * A {@code ProcessAction} is an executable unit of a process.
 */
public interface ProcessAction {

	public void execute(Environment env) throws RoleException;

	/** 
	 * The Empty process action contains no operations.
	 */
	public final class Empty implements ProcessAction {
		public void execute(Environment env) throws RoleException {
		}		
	}
	
	/**
	 * A NativeProcess is a process or sub-process that is written in pure
	 * Java.
	 */
	public final class NativeProcess implements ProcessAction {
		private Class<?> code;
		private Thread thread;
		
		public NativeProcess(Class<?> code, Object...args) {
			this.code = code;
			
			try {
				Class<?>[] argClasses = new Class[args.length];
				for ( int i = 0; i < args.length; i++ )
					argClasses[i] = args[i].getClass();
				Constructor<?> con = code.getConstructor(argClasses);
				Object obj = con.newInstance(args);
				thread = new Thread((Runnable)obj);
			} catch (Exception e) {
				e.printStackTrace();
			}		
		}
		
		public void execute(Environment env) throws RoleException {
			this.thread.start();
		}
		
		public Thread getThread() {
			return this.thread;
		}
		
		public Class<?> getCode() {
			return this.code;
		}
	}
	
	/**
	 * An Assignment process action updates a variable in the environment.
	 */
	public final class Assignment implements ProcessAction {
		private Operation operation;
		private String lhsVariableName;
		private String rhsVariableName;
		private String[] argVariableNames;
		boolean isSimpleAssignment;

		public Assignment(String lhsToken, Operation op, String[] argTokens) {
			this.operation = op;
			this.lhsVariableName = lhsToken;
			this.argVariableNames = argTokens;
			this.isSimpleAssignment = false;
		}

		public Assignment(String lhsToken, String rhsToken) {
			this.lhsVariableName = lhsToken;
			this.rhsVariableName = rhsToken;
			this.isSimpleAssignment = false;
		}

		public Operation getOperation() {
			return operation;
		}

		public String getLhsVariable() {
			return lhsVariableName;
		}

		public String getRhsVariable() {
			return this.rhsVariableName;
		}

		public String[] getArgVariables() {
			return this.argVariableNames;
		}

		public void execute(Environment env) throws RoleException {
			if ( isSimpleAssignment ) {
				Variable lhs = env.getVariable(lhsVariableName);
				Variable rhs = env.getVariable(rhsVariableName);
				lhs.setValue(rhs.getValue());
		} else {
				Object[] args = new Object[argVariableNames.length];
				for ( int i = 0; i < this.argVariableNames.length; i++ )
					args[i] = env.getVariable(argVariableNames[i]);
				// Get roles of calling actor
				// Caller is 
				Actor caller = env.getTask().getActor();
				Set<Role> actorRoles = caller.getRoles();
				checkRights(actorRoles, this.getOperation());
				//Role[] requiredRoles = env.getTask().

				Object obj = operation.execute(args);
				Variable result = env.getVariable(lhsVariableName);
				result.setValue(obj);
			}
		}

		private void checkRights(Set<Role> actorRoles, Operation operation)
		throws RoleException {
			for ( Role role : actorRoles  ) {
				Right[] rights = role.getRights();
				for ( Right r : rights )
					for ( Operation op : r.getOperations() )
						if ( op.equals(operation) )
							return;
			}
			throw new RoleException("Role missing for peration " + operation);
		}
	}

	/**
	 * A Sequence of process actions are executed in serial order.
	 */
	public final class Sequence implements ProcessAction {
		private ProcessAction[] actions;
		public Sequence(ProcessAction...actions) {
			this.actions = actions;
		}
		public ProcessAction[] getActions() {
			return actions;
		}
		public void execute(Environment env) throws RoleException {
			for ( int i = 0; i < actions.length; i++ )
				actions[i].execute(env);
		}
	}

	/**
	 * An Iteration process action is executed in an iterative constuct:
	 * <B>while exp do S</B>.
	 */
	public final class Iteration implements ProcessAction {
		private ProcessAction action;
		private Expression exp;
		public Iteration(ProcessAction action, Expression exp) {
			this.action = action;
			this.exp = exp;
		}
		public ProcessAction getAction() {
			return action;
		}
		public Expression getExp() {
			return exp;
		}
		public void execute(Environment env) throws RoleException {
			while ( exp.evaluate() )
				action.execute(env);
		}
	}

	/**
	 * A Selection process action has the semantics:
	 * <B>if boolExp then S1 else S2</B>.
	 */
	public final class Selection implements ProcessAction {
		private ProcessAction action1, action2;
		private Expression exp;
		public Selection(ProcessAction action1, ProcessAction action2,
				Expression exp) {
			this.action1 = action1;
			this.action2 = action2;
			this.exp = exp;
		}
		public ProcessAction getAction1() {
			return action1;
		}
		public ProcessAction getAction2() {
			return action2;
		}
		public Expression getExp() {
			return exp;
		}
		public void execute(Environment env) throws RoleException {
			if ( exp.evaluate() )
				action1.execute(env);
			else
				action2.execute(env);
		}
	}

	/**
	 * The Simultaneous process action is the combination of process actions
	 * that may be concurrently executed: <B>S1 inparallel S2</B>
	 */
	public final class Simultaneous implements ProcessAction {
		private static class RunnableAction implements Runnable {
			ProcessAction action;
			Environment env;
			RunnableAction(ProcessAction action, Environment env) {
				this.action = action;
				this.env = env;
			}
			public void run() {
				action.equals(env);
			}
			
		}
		private ProcessAction action1, action2;
		public Simultaneous(ProcessAction action1, ProcessAction action2) {
			this.action1 = action1;
			this.action2 = action2;
		}
		public ProcessAction getAction1() {
			return action1;
		}
		public ProcessAction getAction2() {
			return action2;
		}
		public void execute(Environment env) {
			Thread t1 = new Thread(new RunnableAction(action1, env));
			Thread t2 = new Thread(new RunnableAction(action2, env));
			t1.start();
			t2.start();
			try {
				t1.join();
				t2.join();
			} catch (Exception exc) {
				exc.printStackTrace();
			}
		}
	}
}
